//
//  RSDBRecord.m
//  Cellar Tracker
//
//  Created by Johnnie Walker on 09/07/2008.
//  Copyright 2008 Random Sequence. All rights reserved.
//

#import "RSDBRecord.h"
#import "RSDB.h"
#import "RegexKitLite.h"
#import "NSString+Inflection.h"
#import "RSDBResultSet.h"

@implementation RSDBRecord

@synthesize table, database, exists, properties;

- (id) init
{
	self = [super init];
	if (self != nil) {
		properties = [[NSMutableDictionary alloc] init];
		changedProperties = nil;
	}
	return self;
}

- (id)initWithTable:(RSDBTable *)aTable
{
	self = [self init];
	[self setTable:aTable];
	[self setDatabase:[aTable database]];
		
	return self;
}

- (id)initWithTable:(RSDBTable *)aTable resultsFromSet:(RSDBResultSet *)resultSet
{
	self = [self initWithTable:aTable];
	
	id columnValue;
	NSDictionary *columns = [aTable columns];
	
	for (NSString *columnName in [columns allKeys]) {
		NSString *type = [[columns objectForKey:columnName] objectForKey:@"type"];
		if ([type isEqual:@"integer"] || [type isEqual:@"real"]) {
			columnValue = [resultSet numberForColumn:columnName];
		} else {
			columnValue = [resultSet stringForColumn:columnName];
		}
		if (nil != columnValue) {
			[properties setObject:columnValue forKey:columnName];			
		}
	}
	[self setExists:TRUE];
	
	return self;	
}

- (NSString *)description
{
	return [NSString stringWithFormat:@"%@ %@ %@",NSStringFromClass([self class]),[[self table] tableName],[properties description]];
}

- (BOOL)isEqual:(id)object
{
	if ([self class] == [object class] 
		&& [[self table] isEqual:[object table]] 
		&& [[self identifier] isEqual:[object identifier]]) {
		return YES;
	}
	
	return NO;
}

#pragma mark - Accessing Properties

- (NSMutableDictionary *)changedProperties
{
	if (nil == changedProperties) {
		changedProperties = [[NSMutableDictionary alloc] initWithCapacity:[properties count]];					
	}
	return changedProperties;
}

- (id)getProperty:(NSString *)property
{
#ifdef DEBUG_BUILD
	NSLog(@"[%@ getProperty:%@] is depricated. Use [%@ valueForProperty:%@] instead",NSStringFromClass([self class]),property,NSStringFromClass([self class]),property);
#endif
	return [self valueForProperty:property];
}

- (id)valueForProperty:(NSString *)property
{
	return [properties objectForKey:property];	
}

- (void)setValue:(id)value forProperty:(NSString *)property
{
	if ([self.table hasColumn:property]) {
		[self.properties setObject:value forKey:property];
		[self.changedProperties setObject:value forKey:property];	
	}
}

- (id)identifier
{
	return [self valueForProperty:[[self table] primaryKeyColumnName]];
}

- (id)valueForUndefinedKey:(NSString *)key
{
	id value;
	if (value = [self valueForProperty:key]) {
		return value; 
	}
	
	return [super valueForUndefinedKey:key];
}

- (void)setValue:(id)value forUndefinedKey:(NSString *)key
{
	[self setValue:value forProperty:key];
}

- (void)mergeWithDictionary:(NSDictionary *)dictionary
{
	NSEnumerator *enu = [dictionary keyEnumerator];
	id key;
	while (key = [enu nextObject]) {
		//NSLog(@"mergeWithDictionary - key: %@",key);
		[self setValue:[dictionary objectForKey:key] forProperty:key];
	}
}

#pragma mark - Fetch related records

- (NSMutableDictionary *)relations
{
	if (nil == relations) {
		relations = [[NSMutableDictionary alloc] init];
	}
	
	return relations;
}

- (BOOL)hasRelation:(NSString *)relationName
{
	return (nil != [self relationForAlias:relationName]);
}


- (RSDBRelation *)magicHasManyRelationForAlias:(NSString *)alias
{
	RSDBRelation *relation = nil;	
	
	NSString *foreignTableName = alias;
	RSDBTable *foreignTable = [RSDB getTable:foreignTableName database:[self database]];
	NSString *foreignColumnName = [NSString stringWithFormat:@"%@_id",[table tableName]];
	
	// DebugLog(@"foreignTableName: %@ foreignColumnName: %@",foreignTableName,foreignColumnName);
	
	if (foreignTable && [foreignTable hasColumn:foreignColumnName]) {
		NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
								 [[self table] primaryKeyColumnName],RSDBRelationLocalKey,
								 foreignColumnName,RSDBRelationForeignKey,
								 foreignTable,RSDBRelationForeignTableKey,
								 nil];		
		relation = [RSDBRelation relationWithType:RSDBRecordRelationTypeHasMany options:options];
	}		

	return relation;	
}

- (RSDBRelation *)magicBelongsToOneRelationForAlias:(NSString *)alias
{
	RSDBRelation *relation = nil;
	
	NSString *localKey = [NSString stringWithFormat:@"%@_id",alias];
	RSDBTable *foreignTable = [RSDB getTable:alias database:[self database]];
	if (foreignTable && [[self table] hasColumn:localKey]) {
		NSDictionary *options = [NSDictionary dictionaryWithObjectsAndKeys:
								 localKey,RSDBRelationLocalKey,
								 [foreignTable primaryKeyColumnName],RSDBRelationForeignKey,								 
								 [self table],RSDBRelationLocalTableKey,
								 foreignTable,RSDBRelationForeignTableKey,
								 nil];		
		relation = [RSDBRelation relationWithType:RSDBRecordRelationTypeBelongsToOne options:options];
	}	
	
	return relation;
}

- (RSDBRelation *)relationForAlias:(NSString *)alias
{
	return [[self relations] objectForKey:alias];
}

- (void)addRelation:(RSDBRelation *)relation alias:(NSString *)alias
{
	[[self relations] setObject:relation forKey:alias];
}

- (id)getRelatedRecord:(NSString *)alias
{
#ifdef DEBUG_BUILD
	NSLog(@"[%@ getRelatedRecord:%@] is depricated. Use [%@ relatedRecordForProperty:%@] instead",NSStringFromClass([self class]),alias,NSStringFromClass([self class]),alias);
#endif	
	return [self relatedRecordForProperty:alias];	
}

- (id)relatedRecordForProperty:(NSString *)alias
{
//	DebugLog(@"relatedRecordForProperty: %@",alias);
	
	if (![self hasRelation:alias]) {
		RSDBRelation *relation = nil;
		if (relation = [self magicBelongsToOneRelationForAlias:alias]) {
			[self addRelation:relation alias:alias];
		} else {
			return nil;
		}
	}	
		
	return [[self relationForAlias:alias] getOwnerForRecord:self];			
}

- (void)setRelatedRecord:(RSDBRecord *)record forProperty:(NSString *)alias
{
	RSDBRelation *relation = nil;	
	
	if (![self hasRelation:alias]) {		
		if (relation = [self magicBelongsToOneRelationForAlias:alias]) {
			[self addRelation:relation alias:alias];
		}
	}		
	
	// this handles the one-to many type
	if (relation = [self relationForAlias:alias]) {	
		if ([[self table] hasColumn:[relation localKey]]) {
			[self setValue:[record identifier] forKey:[relation localKey]];
		}		
	} 			
}

- (NSArray *)getRelatedRecords:(NSString *)alias
{	
//	DebugLog(@"getRelatedRecords: %@",alias);

	if (![self hasRelation:alias]) {
		RSDBRelation *relation = nil;
		if (relation = [self magicHasManyRelationForAlias:alias]) {
			[self addRelation:relation alias:alias];
		} else {
			return nil;
		}
	}
			
	return [[self relationForAlias:alias] getRecordsOwnedBy:self];			
}

#pragma mark - Finding

+ (RSDBTable *)table
{
	NSString *tableName = [[NSStringFromClass([self class]) stringByReplacingOccurrencesOfRegex:@"^RSDBRecord" withString:@""] tableizedString];	
	return [RSDB getTable:tableName];	
}

+ (RSDBRecord *)find:(id)identifier
{
	RSDBTable *table = [[self class] table];	
	return [table find:identifier];
}

+ (NSArray *)findAll
{
	RSDBTable *table = [[self class] table];	
	return [table findAll];
}

+ (NSArray *)findBySQL:(NSString *)sql, ...
{	
	va_list args;
	va_start(args, sql);
			
	RSDBTable *table = [[self class] table];
	NSArray *results = [table findBySQL:sql arguments:[RSDB argumentsFromSQL:sql args:args]];
	
	va_end(args);	
	
	return results;
}

+ (RSDBRecord *)findOneBySQL:(NSString *)sql, ...;
{
	va_list args;
	va_start(args, sql);
	
	RSDBTable *table = [[self class] table];
	RSDBRecord *result = [table findOneBySQL:sql arguments:[RSDB argumentsFromSQL:sql args:args]];
	
	va_end(args);	
	
	return result;
	
}

#pragma mark - Persistence

- (void)preInsert {}
- (void)postInsert {}
- (void)preUpdate {}
- (void)postUpdate {}
- (void)preSave {}
- (void)postSave {}
- (void)preDelete {}
- (void)postDelete {}

- (BOOL)delete {

	BOOL success = NO;		
	NSString *sql;
	
	if ([self exists]) {
		[self preDelete];	
		sql = [NSString stringWithFormat:@"DELETE FROM %@ WHERE id = ?;",[[self table] tableName]];
		if (success = [[self database] executeUpdate:sql arguments:[NSArray arrayWithObject:[self identifier]]]) {
			[self postDelete];	
		}					
	}	
	
	return success;
}

- (BOOL)save {
	
	if ([self.changedProperties count] || [self identifier] == nil) {
		
		[self preSave];
		
		NSString *sql;
		BOOL success;
		BOOL postSaveCommit = [[self database] beginTransaction];	// returns false if we're already in a transaction
		if ([self exists]) {
			[self preUpdate];
			NSMutableArray *setStatements = [NSMutableArray arrayWithCapacity:[changedProperties count]];
			NSMutableArray *changedValues = [NSMutableArray arrayWithCapacity:[changedProperties count]];
			for (NSString *propertyKey in [changedProperties allKeys]) {
				[setStatements addObject:[NSString stringWithFormat:@"%@ = ?",propertyKey]];
				[changedValues addObject:[changedProperties valueForKey:propertyKey]];
			}
			NSString *statements = [setStatements componentsJoinedByString:@", "];
			sql = [NSString stringWithFormat:@"UPDATE %@ SET %@ WHERE %@ = %@;",[[self table] tableName],statements,[[self table] primaryKeyColumnName],[self identifier]];
#ifdef DEBUG_BUILD
			// NSLog(@"update sql: %@, %@",sql,[changedProperties allValues]);
#endif
			if (success = [[self database] executeUpdate:sql arguments:changedValues]) {
				[self postUpdate];
			}			
		} else {
			[self preInsert];
			// TODO - this is very wrong. The allKeys & allValues arrays aren't in a defined order
			NSMutableArray *columnNamesArray = [NSMutableArray arrayWithCapacity:[changedProperties count]];
			NSMutableArray *qmarks = [NSMutableArray arrayWithCapacity:[changedProperties count]];
			for (NSString *propertyKey in [changedProperties allKeys]) {
				[columnNamesArray addObject:propertyKey];
				[qmarks addObject:@"?"];
			}
			NSString *placeholders = [qmarks componentsJoinedByString:@","];
			NSString *columnNames = [columnNamesArray componentsJoinedByString:@","];			
			sql = [NSString stringWithFormat:@"INSERT INTO %@ (%@) VALUES (%@);",[[self table] tableName],columnNames,placeholders];
#ifdef DEBUG_BUILD
			// NSLog(@"insert sql: %@, %@",sql,[changedProperties allValues]);
#endif
			if (success = [[self database] executeUpdate:sql arguments:[changedProperties allValues]]) {
				[self setValue:[NSNumber numberWithInt:[[self database] lastInsertRowId]] forProperty:[[self table] primaryKeyColumnName]];
				[self postInsert];
			}
			[self setExists:TRUE];
		}
		
		[self postSave];
		if (postSaveCommit) {
			[[self database] commit];			
		}
		return success;
	}
	
	return TRUE;
}

- (void) dealloc
{
	[properties release];
	[changedProperties release];	
	[relations release];
	[super dealloc];
}


@end
